package main

import (
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// Config 结构体用于存储解析后的命令行参数，方便传递
type Config struct {
	InputDir             string
	OutputFile           string
	AppendMode           bool
	ExtensionList        []string // 要包含的扩展名列表 (小写，带.)
	ExcludeExtensionList []string // 要排除的扩展名列表 (小写，带.)
	ExcludeDirList       []string // 要排除的目录相对路径或基本名
	ExcludeFileList      []string // 要排除的文件相对路径或基本名
	IncludeDirList       []string // 要包含的目录相对路径
	AbsInputDir          string   // 输入目录的绝对路径
	AbsOutPath           string   // 输出文件的绝对路径
}

// parseFlags 解析命令行参数并返回 Config 结构体
func parseFlags() *Config {
	// 获取当前工作目录作为默认值
	currentDir, err := os.Getwd()
	if err != nil {
		log.Printf("警告: 无法获取当前工作目录: %v, 使用 '.' 代替。\n", err)
		currentDir = "."
	}

	// 定义默认排除扩展名字符串
	defaultExcludeExts := ".exe,.dll"

	// 定义命令行参数，设置默认值和使用说明 (中文)
	inputDir := flag.String("dir", currentDir, "输入目录路径 (默认: 当前目录)")
	outputFile := flag.String("out", "read_data.txt", "输出文件路径 (默认: read_data.txt)")
	appendMode := flag.Bool("append", false, "追加模式 (默认: 覆盖现有文件)")
	extensions := flag.String("ext", "", "仅包含指定扩展名的文件，用逗号分隔 (如: .go,.txt)，不指定则包含所有文件。")
	excludeExtensions := flag.String("exclude-ext", defaultExcludeExts, "排除指定扩展名的文件，用逗号分隔 (如: .bak,.tmp)。默认排除 .exe,.dll。如果设置为空字符串 \"\"，则不排除任何扩展名。") // 修正了默认值的使用
	excludeDirs := flag.String("exclude-dir", "", "排除的目录名或相对于输入目录的路径，用逗号分隔 (如: .git,node_modules,subdir/tmp)。匹配则跳过该目录及其内容。")
	excludeFiles := flag.String("exclude-file", "", "排除的文件名或相对于输入目录的路径，用逗号分隔 (如: README.md,config/secrets.txt)。匹配则跳过该文件。")
	includeDirs := flag.String("include-dir", "", "仅处理指定子目录下的文件，用逗号分隔 (如: src,internal/pkg)。路径相对于输入目录。不指定则处理输入目录下的所有（非排除）文件。")
	showHelp := flag.Bool("help", false, "显示帮助信息")

	// 解析命令行参数
	flag.Parse()

	// 显示帮助信息并退出
	if *showHelp {
		fmt.Println("目录文件合并工具 - 将指定目录或其子目录下的所有（非排除）文件内容合并到一个文件中。")
		fmt.Println("\n用法:")
		flag.PrintDefaults() // 打印所有 flag 的默认用法和说明
		fmt.Println("\n示例:")
		fmt.Println("  mergedir                              # 合并当前目录下所有非排除文件到read_data.txt (覆盖)")
		fmt.Println("  mergedir -dir /path/to/src            # 合并指定目录下所有非排除文件到read_data.txt (覆盖)")
		fmt.Println("  mergedir -out output.txt -append      # 合并当前目录下所有非排除文件并追加到output.txt")
		fmt.Println("  mergedir -ext .go,.txt                # 仅合并当前目录下扩展名为.go和.txt的文件")
		// 修正示例，明确说明如何清除默认排除
		fmt.Println("  mergedir -exclude-ext .log            # 在默认排除基础上额外排除 .log 文件 (例如，实际会排除 .exe,.dll,.log)") // 默认值是起点
		fmt.Println("  mergedir -exclude-ext \"\"            # 不排除任何扩展名，清除默认设置 (注意引号)")
		fmt.Println("  mergedir -exclude-dir .git,tmp        # 排除当前目录下的.git和tmp目录")
		fmt.Println("  mergedir -exclude-file README.md      # 排除当前目录下的README.md文件")
		fmt.Println("  mergedir -include-dir src,test        # 仅合并当前目录下 'src' 和 'test' 子目录中的文件")
		fmt.Println("  mergedir -dir /project -include-dir frontend,backend # 仅合并 /project 目录下的 frontend 和 backend 子目录中的文件")
		os.Exit(0)
	}

	cfg := &Config{
		InputDir:   *inputDir,
		OutputFile: *outputFile,
		AppendMode: *appendMode,
	}

	// --- 参数解析和预处理 ---

	// 解析要包含的扩展名，转换为小写并确保有 "." 前缀
	if *extensions != "" {
		cfg.ExtensionList = parseExtensionList(*extensions)
	}

	// 解析要排除的扩展名，转换为小写并确保有 "." 前缀
	// 如果用户提供了空字符串 ""，则表示不排除任何扩展名，覆盖默认值
	if *excludeExtensions != "" {
		cfg.ExcludeExtensionList = parseExtensionList(*excludeExtensions)
	} else {
		// 用户明确设置了 -exclude-ext ""，清空排除列表
		cfg.ExcludeExtensionList = []string{}
		log.Println("用户设置 -exclude-ext 为空，不排除任何扩展名。")
	}
	// 如果用户没有设置 -exclude-ext 且默认值非空，则上面的第一个if会执行，使用默认值。
	// 如果用户设置了非空值，上面的第一个if也会执行，使用用户值。
	// 只有用户明确设置 "" 才会走到这里的else。

	// 解析要排除的目录（相对路径或基本名）
	if *excludeDirs != "" {
		cfg.ExcludeDirList = parsePathList(*excludeDirs)
	}

	// 解析要排除的文件（相对路径或基本名）
	if *excludeFiles != "" {
		cfg.ExcludeFileList = parsePathList(*excludeFiles)
	}

	// 解析要包含的目录（相对路径），规范化路径
	if *includeDirs != "" {
		cfg.IncludeDirList = parsePathList(*includeDirs)
		// 规范化包含目录路径，确保没有尾部斜杠
		for i := range cfg.IncludeDirList {
			cfg.IncludeDirList[i] = filepath.Clean(cfg.IncludeDirList[i])
		}
	}

	// --- 输入目录验证 ---

	// 确保输入目录存在且是目录
	inputInfo, err := os.Stat(cfg.InputDir)
	if err != nil {
		log.Fatalf("错误: 无法访问输入目录 '%s': %v\n", cfg.InputDir, err)
	}
	if !inputInfo.IsDir() {
		log.Fatalf("错误: '%s' 不是一个目录。\n", cfg.InputDir)
	}

	// 获取输入目录的绝对路径，用于后续相对路径计算
	absInputDir, err := filepath.Abs(cfg.InputDir)
	if err != nil {
		log.Printf("警告: 无法获取输入目录 '%s' 的绝对路径: %v, 使用原始路径。\n", cfg.InputDir, err)
		cfg.AbsInputDir = cfg.InputDir // Fallback
	} else {
		cfg.AbsInputDir = absInputDir
	}

	// 获取输出文件的绝对路径，用于跳过自身
	absOutPath, err := filepath.Abs(cfg.OutputFile)
	if err != nil {
		log.Printf("警告: 无法获取输出文件 '%s' 的绝对路径: %v, 跳过自身检查可能不准确。\n", cfg.OutputFile, err)
		cfg.AbsOutPath = "!!!ABSOLUTE_PATH_ERROR_PLACEHOLDER!!!" // 使用一个不太可能匹配的路径字符串作为占位符
	} else {
		cfg.AbsOutPath = absOutPath
	}

	return cfg
}

// parseExtensionList 解析逗号分隔的扩展名字符串，转换为小写并确保带 "." 前缀
func parseExtensionList(exts string) []string {
	list := []string{}
	if exts == "" {
		return list
	}
	parts := strings.Split(exts, ",")
	for _, ext := range parts {
		ext = strings.TrimSpace(ext)
		if ext != "" {
			if !strings.HasPrefix(ext, ".") {
				ext = "." + ext
			}
			list = append(list, strings.ToLower(ext))
		}
	}
	return list
}

// parsePathList 解析逗号分隔的路径字符串，去除空白并清理路径
func parsePathList(paths string) []string {
	list := []string{}
	if paths == "" {
		return list
	}
	parts := strings.Split(paths, ",")
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p != "" {
			// 清理路径，处理斜杠等，但不转换为绝对路径
			list = append(list, filepath.Clean(p))
		}
	}
	return list
}

func main() {
	// 设置日志格式，包含时间戳，输出到标准输出
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	log.SetOutput(os.Stdout)

	// 解析参数
	cfg := parseFlags()

	// 创建或打开输出文件
	var outFile *os.File
	var err error
	if cfg.AppendMode {
		// 追加模式: 如果文件不存在则创建，存在则打开并在末尾写入
		outFile, err = os.OpenFile(cfg.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
		if err != nil {
			log.Fatalf("错误: 无法打开输出文件进行追加 '%s': %v\n", cfg.OutputFile, err)
		}
		log.Printf("输出文件 '%s' 以追加模式打开。\n", cfg.OutputFile)
	} else {
		// 覆盖模式: 创建新文件，如果文件已存在则清空内容
		outFile, err = os.Create(cfg.OutputFile)
		if err != nil {
			log.Fatalf("错误: 无法创建输出文件 '%s': %v\n", cfg.OutputFile, err)
		}
		log.Printf("输出文件 '%s' 以覆盖模式打开。\n", cfg.OutputFile)
	}
	defer outFile.Close() // 确保文件在 main 函数结束时关闭

	// --- 写入合并头部信息 ---
	startTime := time.Now().Format("2006-01-02 15:04:05")
	headerInfo := fmt.Sprintf("===== 文件合并开始于: %s =====\n", startTime)
	headerInfo += fmt.Sprintf("===== 输入目录: %s =====\n", cfg.InputDir)
	headerInfo += fmt.Sprintf("===== 输出文件: %s (%s模式) =====\n", cfg.OutputFile, map[bool]string{true: "追加", false: "覆盖"}[cfg.AppendMode])

	// 打印应用的过滤规则
	if len(cfg.IncludeDirList) > 0 {
		headerInfo += fmt.Sprintf("===== 仅包含目录: %s =====\n", strings.Join(cfg.IncludeDirList, ", "))
	} else {
		headerInfo += "===== 包含目录: 所有非排除目录 =====\n"
	}
	if len(cfg.ExcludeDirList) > 0 {
		headerInfo += fmt.Sprintf("===== 排除目录: %s =====\n", strings.Join(cfg.ExcludeDirList, ", "))
	} else {
		headerInfo += "===== 排除目录: 无 =====\n"
	}
	if len(cfg.ExcludeFileList) > 0 {
		headerInfo += fmt.Sprintf("===== 排除文件: %s =====\n", strings.Join(cfg.ExcludeFileList, ", "))
	} else {
		headerInfo += "===== 排除文件: 无 =====\n"
	}
	if len(cfg.ExtensionList) > 0 {
		headerInfo += fmt.Sprintf("===== 仅包含扩展名: %s =====\n", strings.Join(cfg.ExtensionList, ", "))
	} else {
		headerInfo += "===== 包含扩展名: 所有非排除扩展名 =====\n"
	}
	if len(cfg.ExcludeExtensionList) > 0 {
		headerInfo += fmt.Sprintf("===== 排除扩展名: %s =====\n", strings.Join(cfg.ExcludeExtensionList, ", "))
	} else {
		headerInfo += "===== 排除扩展名: 无 =====\n"
	}

	headerInfo += "\n" // 在头部信息后添加一个空行
	_, err = outFile.WriteString(headerInfo)
	if err != nil {
		log.Printf("警告: 写入头部信息失败: %v\n", err)
	}

	// --- 遍历并处理文件 ---

	// 计数器
	fileCount := 0         // 成功处理的文件数
	totalBytes := int64(0) // 写入的总字节数
	skippedFileCount := 0  // 因文件过滤规则跳过的文件数 (包括输出文件本身)
	skippedDirCount := 0   // 因目录过滤规则跳过的目录数

	log.Printf("开始遍历处理目录: %s\n", cfg.InputDir)

	// 定义一个匿名函数作为 filepath.Walk 的处理函数
	walkFunc := func(path string, info os.FileInfo, walkErr error) error {
		// 处理 filepath.Walk 自身遇到的错误 (如权限不足)
		if walkErr != nil {
			log.Printf("警告: 访问 '%s' 时出错: %v\n", path, walkErr)
			// 返回 nil 允许 Walk 继续处理其他路径
			return nil
		}

		// 计算当前路径相对于输入目录的相对路径
		// 使用 filepath.Rel 和 filepath.Clean 规范化
		relPath, err := filepath.Rel(cfg.AbsInputDir, path)
		if err != nil {
			log.Printf("警告: 无法计算相对路径 '%s' (相对于 '%s'): %v, 跳过此路径。\n", path, cfg.AbsInputDir, err)
			if info.IsDir() { // 如果是目录，跳过整个目录
				skippedDirCount++
				return filepath.SkipDir
			} else { // 如果是文件，跳过文件
				skippedFileCount++
				return nil
			}
		}
		relPath = filepath.Clean(relPath)

		// 0. 跳过输入目录本身（relPath == "."）
		if relPath == "." {
			// 这是根目录，如果指定了包含目录，则只处理包含目录下的内容
			if info.IsDir() && len(cfg.IncludeDirList) > 0 {
				// log.Printf("跳过根目录 '%s' 内容（因指定了包含目录），仅处理指定的子目录。", path) // 这个日志可能太频繁
			}
			return nil // 继续处理其子目录
		}

		// 1. 跳过输出文件本身，防止无限增长
		absPath, err := filepath.Abs(path)
		if err != nil {
			// 如果获取绝对路径失败，仍使用原始路径进行比较（准确性降低）
			absPath = path
		}
		if absPath == cfg.AbsOutPath {
			log.Printf("跳过输出文件本身: %s\n", path)
			skippedFileCount++
			// 如果输出路径恰好是目录（极不可能），我们也不想遍历它
			if info.IsDir() {
				return filepath.SkipDir
			}
			return nil // 文件则直接跳过
		}

		// 2. 处理目录过滤
		if info.IsDir() {
			// 检查目录是否在排除列表中 (检查相对路径或基本名)
			if isExcludedDirectory(relPath, path, cfg.ExcludeDirList) {
				log.Printf("跳过目录 (匹配排除规则): %s (相对路径: %s)\n", path, relPath)
				skippedDirCount++
				return filepath.SkipDir // 跳过整个目录及其内容
			}

			// 检查目录是否在包含目录列表中 (如果指定了包含目录)
			// 如果指定了包含目录，那么只有包含目录本身或者包含目录下的子目录才应该被继续遍历
			if len(cfg.IncludeDirList) > 0 {
				isIncluded := false
				for _, includedDir := range cfg.IncludeDirList {
					// 如果当前相对路径是包含目录本身，或者以包含目录路径开头+分隔符
					if relPath == includedDir || strings.HasPrefix(relPath, includedDir+string(filepath.Separator)) {
						isIncluded = true
						break
					}
				}
				if !isIncluded {
					// log.Printf("跳过目录 (不在包含目录中): %s (相对路径: %s)\n", path, relPath) // 这个日志可能太频繁
					skippedDirCount++
					return filepath.SkipDir // 跳过不在包含目录中的目录及其内容
				}
			}

			// 如果是目录且未被排除，且在包含目录范围内，继续遍历其内容
			return nil
		}

		// 3. 处理文件过滤 (info.IsDir() 为 false)

		// 检查文件是否在排除列表中 (检查相对路径或基本名)
		if isExcludedFile(relPath, path, cfg.ExcludeFileList) {
			log.Printf("跳过文件 (匹配排除文件规则): %s (相对路径: %s)\n", path, relPath)
			skippedFileCount++
			return nil // 跳过当前文件，继续处理其他文件
		}

		// 检查文件扩展名是否在排除列表中 (检查扩展名)
		if isExcludedExtension(path, cfg.ExcludeExtensionList) {
			log.Printf("跳过文件 (匹配排除扩展名规则): %s (相对路径: %s, 扩展名: %s)\n", path, relPath, strings.ToLower(filepath.Ext(path)))
			skippedFileCount++
			return nil // 跳过当前文件
		}

		// 检查包含目录过滤 (如果指定了 include-dir)
		if len(cfg.IncludeDirList) > 0 {
			// 文件必须位于某个包含目录内
			if !isIncludedInDir(relPath, cfg.IncludeDirList) {
				// log.Printf("跳过文件 (不在包含目录中): %s (相对路径: %s)\n", path, relPath) // 这个日志可能太频繁
				skippedFileCount++
				return nil // 跳过不在包含目录中的文件
			}
		}

		// 检查扩展名过滤 (如果指定了 ext)
		if len(cfg.ExtensionList) > 0 {
			if !matchesExtension(path, cfg.ExtensionList) {
				// log.Printf("跳过文件 (扩展名不匹配包含规则): %s (相对路径: %s, 扩展名: %s)\n", path, relPath, strings.ToLower(filepath.Ext(path))) // 这个日志可能太频繁
				skippedFileCount++
				return nil // 不处理不匹配扩展名的文件
			}
		}

		// --- 文件处理流程 ---

		// 如果文件通过所有过滤检查，则处理该文件
		log.Printf("正在处理文件: %s (相对路径: %s)\n", path, relPath)

		// 写入文件头部信息到输出文件
		fileHeader := fmt.Sprintf("\n\n===== 文件: %s =====\n\n", relPath)
		_, err = outFile.WriteString(fileHeader)
		if err != nil {
			log.Printf("警告: 写入文件头失败 '%s': %v\n", relPath, err)
			// 继续处理下一个文件
			return nil
		}

		// 打开源文件
		file, err := os.Open(path)
		if err != nil {
			log.Printf("警告: 无法打开文件 '%s': %v\n", path, err)
			// 继续处理下一个文件
			return nil
		}
		defer file.Close() // 确保源文件关闭

		// 复制文件内容到输出文件
		written, err := io.Copy(outFile, file)
		if err != nil {
			log.Printf("警告: 读取或写入文件内容时出错 '%s': %v\n", path, err)
			// 继续处理下一个文件
			return nil
		}

		// 更新计数器
		fileCount++
		totalBytes += written

		return nil // 返回 nil 继续处理下一个文件
	}

	// 开始遍历目录
	err = filepath.Walk(cfg.InputDir, walkFunc)

	// 检查 filepath.Walk 自身是否发生错误（中断遍历的错误）
	if err != nil {
		log.Printf("错误: 遍历目录时发生中断错误: %v\n", err)
	}

	// --- 写入结束标记 ---
	endTime := time.Now().Format("2006-01-02 15:04:05")
	footer := fmt.Sprintf("\n\n===== 合并结束于: %s =====\n", endTime)
	_, err = outFile.WriteString(footer)
	if err != nil {
		log.Printf("警告: 写入结束标记失败: %v\n", err)
	}

	// --- 打印总结信息 ---
	fmt.Printf("\n---------------- 总结 ----------------\n")
	fmt.Printf("输入目录: %s\n", cfg.InputDir)
	fmt.Printf("输出文件: %s\n", cfg.OutputFile)
	fmt.Printf("模式: %s\n", map[bool]string{true: "追加", false: "覆盖"}[cfg.AppendMode])

	fmt.Printf("应用的过滤规则:\n")
	if len(cfg.IncludeDirList) > 0 {
		fmt.Printf("  仅包含目录: %s\n", strings.Join(cfg.IncludeDirList, ", "))
	} else {
		fmt.Printf("  包含目录: 所有非排除目录\n")
	}
	if len(cfg.ExcludeDirList) > 0 {
		fmt.Printf("  排除目录: %s\n", strings.Join(cfg.ExcludeDirList, ", "))
	} else {
		fmt.Printf("  排除目录: 无\n")
	}
	if len(cfg.ExcludeFileList) > 0 {
		fmt.Printf("  排除文件: %s\n", strings.Join(cfg.ExcludeFileList, ", "))
	} else {
		fmt.Printf("  排除文件: 无\n")
	}
	if len(cfg.ExtensionList) > 0 {
		fmt.Printf("  仅包含扩展名: %s\n", strings.Join(cfg.ExtensionList, ", "))
	} else {
		fmt.Printf("  包含扩展名: 所有非排除扩展名\n")
	}
	if len(cfg.ExcludeExtensionList) > 0 {
		fmt.Printf("  排除扩展名: %s\n", strings.Join(cfg.ExcludeExtensionList, ", "))
	} else {
		fmt.Printf("  排除扩展名: 无\n")
	}

	fmt.Printf("\n")
	fmt.Printf("总计处理文件: %d 个\n", fileCount)
	fmt.Printf("总计因过滤跳过文件: %d 个 (包括输出文件本身)\n", skippedFileCount)
	fmt.Printf("总计因过滤跳过目录: %d 个\n", skippedDirCount)
	fmt.Printf("总计写入字节: %d 字节\n", totalBytes)
	fmt.Printf("--------------------------------------\n")

	log.Println("程序执行完毕。")
}

// Helper functions for filtering logic

// isExcludedDirectory 检查目录是否匹配排除规则
// 检查相对路径是否等于排除规则，或目录的基本名是否等于排除规则
func isExcludedDirectory(relPath, absPath string, excludeDirList []string) bool {
	if len(excludeDirList) == 0 {
		return false
	}
	dirBaseName := filepath.Base(absPath)
	for _, excluded := range excludeDirList {
		// 检查相对路径是否完全匹配排除规则
		if relPath == excluded {
			return true
		}
		// 检查目录的基本名是否完全匹配排除规则 (兼容旧的行为，排除所有同名目录)
		if dirBaseName == excluded {
			return true
		}
	}
	return false
}

// isExcludedFile 检查文件是否匹配排除规则
// 检查相对路径是否等于排除规则，或文件的基本名是否等于排除规则
func isExcludedFile(relPath, absPath string, excludeFileList []string) bool {
	if len(excludeFileList) == 0 {
		return false
	}
	fileBaseName := filepath.Base(absPath)
	for _, excluded := range excludeFileList {
		// 检查相对路径是否完全匹配排除规则
		if relPath == excluded {
			return true
		}
		// 检查文件的基本名是否完全匹配排除规则 (兼容旧的行为，排除所有同名文件)
		if fileBaseName == excluded {
			return true
		}
	}
	return false
}

// isExcludedExtension 检查文件扩展名是否匹配排除扩展名规则 (不区分大小写)
func isExcludedExtension(path string, excludeExtensionList []string) bool {
	if len(excludeExtensionList) == 0 {
		return false
	}
	ext := strings.ToLower(filepath.Ext(path))
	for _, excludedExt := range excludeExtensionList {
		if ext == excludedExt {
			return true
		}
	}
	return false
}

// isIncludedInDir 检查相对路径的文件是否位于包含目录列表中 (如果 includeDirList 非空)
func isIncludedInDir(relPath string, includeDirList []string) bool {
	if len(includeDirList) == 0 {
		return true // 如果没有指定包含目录，则所有文件都视为被包含
	}
	for _, includedDir := range includeDirList {
		// 文件路径必须是包含目录本身（虽然 walkFunc 的文件判断不会是目录本身），
		// 或者以包含目录路径开头+分隔符
		if relPath == includedDir || strings.HasPrefix(relPath, includedDir+string(filepath.Separator)) {
			return true
		}
	}
	return false // 不在任何包含目录中
}

// matchesExtension 检查文件扩展名是否匹配包含扩展名规则 (不区分大小写)
func matchesExtension(path string, extensionList []string) bool {
	if len(extensionList) == 0 {
		return true // 如果没有指定包含扩展名，则所有文件都视为匹配
	}
	ext := strings.ToLower(filepath.Ext(path))
	for _, allowedExt := range extensionList {
		if ext == allowedExt {
			return true
		}
	}
	return false // 不匹配任何包含扩展名
}
